package com.ccteam.fluidmusic.fluidmusic.common.utils

import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.os.ResultReceiver
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.PlaybackStateCompat
import com.ccteam.fluidmusic.fluidmusic.common.PlaylistManager
import com.ccteam.fluidmusic.fluidmusic.media.extensions.addToMediaSource
import com.ccteam.fluidmusic.fluidmusic.media.extensions.id
import com.ccteam.fluidmusic.fluidmusic.media.library.BrowseTree
import com.google.android.exoplayer2.ControlDispatcher
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import kotlinx.coroutines.*

/**
 *
 * @author Xiaoc
 * @since 2021/1/2
 *
 * FluidMusic的播放准备器
 * 当客户端调用对应的 [android.support.v4.media.session.MediaControllerCompat] 的播放方法时会调用对应相应方法
 * 该类主要进行播放内容到播放器内容的装填操作
 */
class FluidMusicPlaybackPreparer(
    private val defaultFactory: DataSource.Factory,
    private val playlistStorage: PlaylistStorage,
    private val browseTree: BrowseTree,
    private val exoPlayer: ExoPlayer,
    private val dataSourceFactory: DataSource.Factory,
    private val mediaSource: ConcatenatingMediaSource
): MediaSessionConnector.PlaybackPreparer{

    @Volatile private var isPassPrepare: Boolean = false

    // 协程组件
    private val serviceJob = SupervisorJob()
    private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)

    override fun onCommand(
        player: Player,
        controlDispatcher: ControlDispatcher,
        command: String,
        extras: Bundle?,
        cb: ResultReceiver?
    ): Boolean {
        return false
    }

    override fun getSupportedPrepareActions(): Long =
        PlaybackStateCompat.ACTION_PREPARE_FROM_MEDIA_ID or
                PlaybackStateCompat.ACTION_PLAY_FROM_MEDIA_ID or
                PlaybackStateCompat.ACTION_PREPARE_FROM_SEARCH or
                PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH or
                PlaybackStateCompat.ACTION_PREPARE_FROM_URI or
                PlaybackStateCompat.ACTION_PLAY_FROM_URI or
                PlaybackStateCompat.ACTION_PREPARE or
                PlaybackStateCompat.ACTION_PLAY

    /**
     * 当客户端调用
     * [android.support.v4.media.session.MediaControllerCompat.TransportControls.play] 或
     * [android.support.v4.media.session.MediaControllerCompat.TransportControls.prepare]
     * 时会调用此方法（前提是此时播放器处于初始化状态，因为[MediaSessionConnector.onPrepare]做了对应处理）
     *
     * 此方法被调用后会进行数据库检索播放列表并装备到 [ExoPlayer]
     * @see com.ccteam.fluidmusic.fluidmusic.common.MusicServiceConnection 在连接成功后会调用 prepare()
     */
    override fun onPrepare(playWhenReady: Boolean) {
        serviceScope.launch {
            // 从数据库和DataStore中获得播放列表内容
            val playlist = playlistStorage.getPlaylist()
            val currentPlayingModel = playlistStorage.loadCurrentPlayingModel()
            val prepareData = preparePlaylist(playlist,
                currentPlayingModel.currentQueueId,
                currentPlayingModel.currentPosition)

            if(!isPassPrepare){
                preparePlayer(prepareData,playWhenReady)
            }
            exoPlayer.repeatMode = currentPlayingModel.currentRepeatMode
            exoPlayer.shuffleModeEnabled = currentPlayingModel.currentShuffleMode
        }
    }

    /**
     * 当客户端调用
     * [android.support.v4.media.session.MediaControllerCompat.TransportControls.playFromMediaId] 或
     * [android.support.v4.media.session.MediaControllerCompat.TransportControls.prepareFromMediaId]
     * 时会调用此方法
     *
     * 此方法调用后会根据传来的 mediaId 和 parentId 到 [BrowseTree] 中进行对应检索需要加载到播放器的具体播放内容
     */
    override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
        val parentId = extras?.getString(MediaIDHelper.PARENT_ID_TAG)

        parentId ?: return

        serviceScope.launch {
            val prepareData = preparePlaylist(mediaId,parentId)

            preparePlayer(prepareData,playWhenReady)
            // 存储播放列表到Room数据库
            prepareData?.let {
                savePlaylistToStorage(it.readyPlaylist)
            }
        }

    }

    override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {

    }

    override fun onPrepareFromUri(uri: Uri, playWhenReady: Boolean, extras: Bundle?) {
    }

    /**
     * 准备播放列表到 [ExoPlayer]
     *
     * 使用协程进行操作，待操作完成后返回 [PrepareData] 信息，其中包含一些要准备的信息
     * 这里通过 [parentId] 从 [BrowseTree] 树中找到对应的待播列表
     * 然后通过 [mediaId] 找到需要播放的当前歌曲的索引，替换播放列表 [PlaylistManager] 的播放内容
     * 然后包装后返回
     * @return 加载完成后的包装信息用于播放，可以返回 @null
     */
    private suspend fun preparePlaylist(mediaId: String,
                                        parentId: String): PrepareData?{
        return withContext(Dispatchers.Default){
            val playlistList = browseTree.getMediaList(parentId)

            playlistList?.let { playlist ->
                // 根据parentId和mediaId找到当前待播的列表和当前项的索引
                var initialWindowIndex = 0
                for(index in playlist.indices){
                    if(playlist[index].id == mediaId){
                        initialWindowIndex = index
                        break
                    }
                }

                PlaylistManager.replaceAll(playlist)

                mediaSource.clear()
                mediaSource.addMediaSources(playlist.addToMediaSource(dataSourceFactory,defaultFactory))

                return@withContext PrepareData(mediaSource,initialWindowIndex,0,playlist)
            } ?: return@withContext null

        }
    }

    /**
     * 准备播放列表到 [ExoPlayer]
     *
     * @see preparePlaylist
     * 属于准备列表的另一种实现方式，提供已经准备好的播放内容列表和对应内容
     */
    private suspend fun preparePlaylist(playlist: List<MediaMetadataCompat>,
                                        currentPosition: Long,
                                        positionMs: Long): PrepareData?{
        return withContext(Dispatchers.Default){
            if(playlist.isEmpty()){
                return@withContext null
            }
            PlaylistManager.replaceAll(playlist)
            mediaSource.clear()
            mediaSource.addMediaSources(playlist.addToMediaSource(dataSourceFactory,defaultFactory))
            return@withContext PrepareData(mediaSource,currentPosition.toInt(),positionMs,playlist)
        }
    }

    /**
     * 存储播放列表到Room的方法
     */
    private suspend fun savePlaylistToStorage(playlistMetadata: List<MediaMetadataCompat>){
        playlistStorage.savePlaylist(playlistMetadata)
    }

    /**
     * 准备播放器Player
     * 通过对 [ExoPlayer] 设置 [ExoPlayer.setMediaSource] 并准备进行播放的准备
     * @param prepareData 准备的数据，其中含有播放的资源数据，初始播放内容等
     * @param playWhenReady 是否准备完毕后就开始播放
     */
    private fun preparePlayer(prepareData: PrepareData?, playWhenReady: Boolean){
        prepareData?.let {
            exoPlayer.setMediaSource(prepareData.mediaSource,false)
            exoPlayer.prepare()
            exoPlayer.seekTo(prepareData.initialWindowIndex, prepareData.positionMs)
            exoPlayer.playWhenReady = playWhenReady
        }?: run {
            exoPlayer.prepare()
        }
    }

    /**
     * 关闭协程操作
     */
    fun close(){
        serviceJob.cancel()
    }

    /**
     * 准备播放的一些数据包装
     * @param mediaSource 资源数据
     * @param initialWindowIndex 初始要播放的索引index
     * @param positionMs 要从什么时间开始播放
     * @param readyPlaylist 准备播放的播放列表清单
     */
    private data class PrepareData(
        val mediaSource: MediaSource,
        val initialWindowIndex: Int,
        val positionMs: Long,
        val readyPlaylist: List<MediaMetadataCompat>
    )
}